/**
 * @ignore
 * dom-attr
 * @author yiminghe@gmail.com, lifesinger@gmail.com
 */

var util = require('util');
var Dom = require('./api');
var doc = document,
    NodeType = Dom.NodeType,
    docElement = doc && doc.documentElement,
    EMPTY = '',
    nodeName = Dom.nodeName,
    R_BOOLEAN = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
    R_FOCUSABLE = /^(?:button|input|object|select|textarea)$/i,
    R_CLICKABLE = /^a(?:rea)?$/i,
    R_INVALID_CHAR = /:|^on/,
    R_RETURN = /\r/g,

    attrFix = {},

    attrFn = {
        val: 1,
        css: 1,
        html: 1,
        text: 1,
        data: 1,
        width: 1,
        height: 1,
        offset: 1,
        scrollTop: 1,
        scrollLeft: 1
    },

    attrHooks = {
        // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
        tabindex: {
            get: function (el) {
                // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
                var attributeNode = el.getAttributeNode('tabindex');
                return attributeNode && attributeNode.specified ?
                    parseInt(attributeNode.value, 10) :
                        R_FOCUSABLE.test(el.nodeName) ||
                    R_CLICKABLE.test(el.nodeName) && el.href ?
                    0 :
                    undefined;
            }
        }
    },

    propFix = {
        hidefocus: 'hideFocus',
        tabindex: 'tabIndex',
        readonly: 'readOnly',
        'for': 'htmlFor',
        'class': 'className',
        maxlength: 'maxLength',
        cellspacing: 'cellSpacing',
        cellpadding: 'cellPadding',
        rowspan: 'rowSpan',
        colspan: 'colSpan',
        usemap: 'useMap',
        frameborder: 'frameBorder',
        contenteditable: 'contentEditable'
    },

// Hook for boolean attributes
// if bool is false
// - standard browser returns null
// - ie<8 return false
// - norm to undefined
    boolHook = {
        get: function (elem, name) {
            // 转发到 prop 方法
            return Dom.prop(elem, name) ?
                // 根据 w3c attribute , true 时返回属性名字符串
                name.toLowerCase() :
                undefined;
        },
        set: function (elem, value, name) {
            var propName;
            if (value === false) {
                // Remove boolean attributes when set to false
                Dom.removeAttr(elem, name);
            } else {
                // 直接设置 true,因为这是 bool 类属性
                propName = propFix[ name ] || name;
                if (propName in elem) {
                    // Only set the IDL specifically if it already exists on the element
                    elem[ propName ] = true;
                }
                elem.setAttribute(name, name.toLowerCase());
            }
            return name;
        }
    },

    propHooks = {
    },

// get attribute value from attribute node, only for ie
    attrNodeHook = {
    },

    valHooks = {

        select: {
            // fix for multiple select
            get: function (elem) {
                var index = elem.selectedIndex,
                    options = elem.options,
                    ret,
                    i,
                    len,
                    one = (String(elem.type) === 'select-one');

                // Nothing was selected
                if (index < 0) {
                    return null;
                } else if (one) {
                    return Dom.val(options[index]);
                }

                // Loop through all the selected options
                ret = [];
                i = 0;
                len = options.length;
                for (; i < len; ++i) {
                    if (options[i].selected) {
                        ret.push(Dom.val(options[i]));
                    }
                }
                // Multi-Selects return an array
                return ret;
            },

            set: function (elem, value) {
                var values = util.makeArray(value),
                    opts = elem.options;
                util.each(opts, function (opt) {
                    opt.selected = util.inArray(Dom.val(opt), values);
                });

                if (!values.length) {
                    elem.selectedIndex = -1;
                }
                return values;
            }
        }

    };

// Radios and checkboxes getter/setter
util.each(['radio', 'checkbox'], function (r) {
    valHooks[r] = {
        get: function (elem) {
            // Handle the case where in Webkit '' is returned instead of 'on'
            // if a value isn't specified
            return elem.getAttribute('value') === null ? 'on' : elem.value;
        },
        set: function (elem, value) {
            if (util.isArray(value)) {
                elem.checked = util.inArray(Dom.val(elem), value);
                return 1;
            }
            return undefined;
        }
    };
});

// IE7- 下，需要用 cssText 来获取
// 所有浏览器统一下, attr('style') 标准浏览器也不是 undefined
attrHooks.style = {
    get: function (el) {
        return el.style.cssText;
    }
};

function toStr(value) {
    return value == null ? '' : value + '';

}

function getProp(elem, name) {
    name = propFix[name] || name;
    var hook = propHooks[name];
    if (hook && hook.get) {
        return hook.get(elem, name);
    } else {
        return elem[name];
    }
}

util.mix(Dom,
    /**
     * @override KISSY.DOM
     * @class
     * @singleton
     */
    {

        _valHooks: valHooks,

        _propFix: propFix,

        _attrHooks: attrHooks,

        _propHooks: propHooks,

        _attrNodeHook: attrNodeHook,

        _attrFix: attrFix,

        /**
         * Get the value of a property for the first element in the set of matched elements.
         * or
         * Set one or more properties for the set of matched elements.
         * @param {HTMLElement[]|String|HTMLElement} selector matched elements
         * @param {String|Object} name The name of the property to set or A map of property-value pairs to set.
         * @param {*} [value] A value to set for the property.
         * @return {String|undefined|Boolean}
         */
        prop: function (selector, name, value) {
            var elems = Dom.query(selector),
                i,
                elem,
                hook;

            // supports hash
            if (typeof name === 'object') {
                util.each(name, function (v, k) {
                    Dom.prop(elems, k, v);
                });
                return undefined;
            }

            // Try to normalize/fix the name
            name = propFix[ name ] || name;
            hook = propHooks[ name ];
            if (value !== undefined) {
                for (i = elems.length - 1; i >= 0; i--) {
                    elem = elems[i];
                    if (hook && hook.set) {
                        hook.set(elem, value, name);
                    } else {
                        elem[ name ] = value;
                    }
                }
            } else {
                if (elems.length) {
                    return getProp(elems[0], name);
                }
            }
            return undefined;
        },

        /**
         * Whether one of the matched elements has specified property name
         * @param {HTMLElement[]|String|HTMLElement} selector 元素
         * @param {String} name The name of property to test
         * @return {Boolean}
         */
        hasProp: function (selector, name) {
            var elems = Dom.query(selector),
                i,
                len = elems.length,
                el;
            for (i = 0; i < len; i++) {
                el = elems[i];
                if (getProp(el, name) !== undefined) {
                    return true;
                }
            }
            return false;
        },

        /**
         * Remove a property for the set of matched elements.
         * @param {HTMLElement[]|String|HTMLElement} selector matched elements
         * @param {String} name The name of the property to remove.
         */
        removeProp: function (selector, name) {
            name = propFix[ name ] || name;
            var elems = Dom.query(selector),
                i,
                el;
            for (i = elems.length - 1; i >= 0; i--) {
                el = elems[i];
                try {
                    el[ name ] = undefined;
                    delete el[ name ];
                } catch (e) {
                }
            }
        },

        /**
         * Get the value of an attribute for the first element in the set of matched elements.
         * or
         * Set one or more attributes for the set of matched elements.
         * @param {HTMLElement[]|HTMLElement|String} selector matched elements
         * @param {String|Object} name The name of the attribute to set. or A map of attribute-value pairs to set.
         * @param {*} [val] A value to set for the attribute.
         * @param {Boolean} [pass] internal use by anim
         * @return {String|undefined}
         */
        attr: function (selector, name, val, /*internal use by anim/fx*/pass) {
            /*
             Hazards From Caja Note:

             - In IE[67], el.setAttribute doesn't work for attributes like
             'class' or 'for'.  IE[67] expects you to set 'className' or
             'htmlFor'.  Caja use setAttributeNode solves this problem.

             - In IE[67], <input> elements can shadow attributes.  If el is a
             form that contains an <input> named x, then el.setAttribute(x, y)
             will set x's value rather than setting el's attribute.  Using
             setAttributeNode solves this problem.

             - In IE[67], the style attribute can only be modified by setting
             el.style.cssText.  Neither setAttribute nor setAttributeNode will
             work.  el.style.cssText isn't bullet-proof, since it can be
             shadowed by <input> elements.

             - In IE[67], you can never change the type of an <button> element.
             setAttribute('type') silently fails, but setAttributeNode
             throws an exception.  caja : the silent failure. KISSY throws error.

             - In IE[67], you can never change the type of an <input> element.
             setAttribute('type') throws an exception.  We want the exception.

             - In IE[67], setAttribute is case-sensitive, unless you pass 0 as a
             3rd argument.  setAttributeNode is case-insensitive.

             - Trying to set an invalid name like ':' is supposed to throw an
             error.  In IE[678] and Opera 10, it fails without an error.
             */

            var els = Dom.query(selector),
                attrNormalizer,
                i,
                el = els[0],
                ret;

            // supports hash
            if (typeof name === 'object') {
                pass = val;
                for (var k in name) {
                    Dom.attr(els, k, name[k], pass);
                }
                return undefined;
            }

            // attr functions
            if (pass && attrFn[name]) {
                return Dom[name](selector, val);
            }

            // scrollLeft
            name = name.toLowerCase();

            if (pass && attrFn[name]) {
                return Dom[name](selector, val);
            }

            // custom attrs
            name = attrFix[name] || name;

            if (R_BOOLEAN.test(name)) {
                attrNormalizer = boolHook;
            } else if (R_INVALID_CHAR.test(name)) {
                // only old ie?
                attrNormalizer = attrNodeHook;
            } else {
                attrNormalizer = attrHooks[name];
            }

            if (val === undefined) {
                if (el && el.nodeType === NodeType.ELEMENT_NODE) {
                    // browsers index elements by id/name on forms, give priority to attributes.
                    if (nodeName(el) === 'form') {
                        attrNormalizer = attrNodeHook;
                    }
                    if (attrNormalizer && attrNormalizer.get) {
                        return attrNormalizer.get(el, name);
                    }

                    ret = el.getAttribute(name);

                    if (ret === '') {
                        var attrNode = el.getAttributeNode(name);
                        if (!attrNode || !attrNode.specified) {
                            return undefined;
                        }
                    }

                    // standard browser non-existing attribute return null
                    // ie<8 will return undefined , because it return property
                    // so norm to undefined
                    return ret === null ? undefined : ret;
                }
            } else {
                for (i = els.length - 1; i >= 0; i--) {
                    el = els[i];
                    if (el && el.nodeType === NodeType.ELEMENT_NODE) {
                        if (nodeName(el) === 'form') {
                            attrNormalizer = attrNodeHook;
                        }
                        if (attrNormalizer && attrNormalizer.set) {
                            attrNormalizer.set(el, val, name);
                        } else {
                            // convert the value to a string (all browsers do this but IE)
                            el.setAttribute(name, EMPTY + val);
                        }
                    }
                }
            }
            return undefined;
        },

        /**
         * Remove an attribute from each element in the set of matched elements.
         * @param {HTMLElement[]|String} selector matched elements
         * @param {String} name An attribute to remove
         */
        removeAttr: function (selector, name) {
            name = name.toLowerCase();
            name = attrFix[name] || name;
            var els = Dom.query(selector),
                propName,
                el, i;
            for (i = els.length - 1; i >= 0; i--) {
                el = els[i];
                if (el.nodeType === NodeType.ELEMENT_NODE) {
                    el.removeAttribute(name);
                    // Set corresponding property to false for boolean attributes
                    if (R_BOOLEAN.test(name) && (propName = propFix[ name ] || name) in el) {
                        el[ propName ] = false;
                    }
                }
            }
        },

        /**
         * Whether one of the matched elements has specified attribute
         * @method
         * @param {HTMLElement[]|String} selector matched elements
         * @param {String} name The attribute to be tested
         * @return {Boolean}
         */
        hasAttr: docElement && !docElement.hasAttribute ?
            function (selector, name) {
                name = name.toLowerCase();
                var elems = Dom.query(selector),
                    i, el,
                    attrNode;
                // from ppk :http://www.quirksmode.org/dom/w3c_core.html
                // IE5-7 doesn't return the value of a style attribute.
                // var $attr = el.attributes[name];
                for (i = 0; i < elems.length; i++) {
                    el = elems[i];
                    attrNode = el.getAttributeNode(name);
                    if (attrNode && attrNode.specified) {
                        return true;
                    }
                }
                return false;
            } :
            function (selector, name) {
                var elems = Dom.query(selector), i,
                    len = elems.length;
                for (i = 0; i < len; i++) {
                    //使用原生实现
                    if (elems[i].hasAttribute(name)) {
                        return true;
                    }
                }
                return false;
            },

        /**
         * Get the current value of the first element in the set of matched elements.
         * or
         * Set the value of each element in the set of matched elements.
         * @param {HTMLElement[]|String} selector matched elements
         * @param {String|String[]} [value] A string of text or an array of strings corresponding to the value of each matched element to set as selected/checked.
         * @return {undefined|String|String[]|Number}
         */
        val: function (selector, value) {
            var hook, ret, elem, els, i, val;

            //getter
            if (value === undefined) {

                elem = Dom.get(selector);

                if (elem) {
                    hook = valHooks[ nodeName(elem) ] || valHooks[ elem.type ];

                    if (hook && 'get' in hook &&
                        (ret = hook.get(elem, 'value')) !== undefined) {
                        return ret;
                    }

                    ret = elem.value;

                    return typeof ret === 'string' ?
                        // handle most common string cases
                        ret.replace(R_RETURN, '') :
                        // handle cases where value is null/undefined or number
                            ret == null ? '' : ret;
                }

                return undefined;
            }

            els = Dom.query(selector);
            for (i = els.length - 1; i >= 0; i--) {
                elem = els[i];
                if (elem.nodeType !== 1) {
                    return undefined;
                }

                val = value;

                // Treat null/undefined as ''; convert numbers to string
                if (val == null) {
                    val = '';
                } else if (typeof val === 'number') {
                    val += '';
                } else if (util.isArray(val)) {
                    val = util.map(val, toStr);
                }

                hook = valHooks[nodeName(elem)] || valHooks[elem.type];
                var hookHasSet = hook && ('set' in hook);
                // If set returns undefined, fall back to normal setting
                if (!hookHasSet || (hook.set(elem, val, 'value') === undefined)) {
                    elem.value = val;
                }
            }
            return undefined;
        },

        /**
         * Get the combined text contents of each element in the set of matched elements, including their descendants.
         * or
         * Set the content of each element in the set of matched elements to the specified text.
         * @param {HTMLElement[]|HTMLElement|String} selector matched elements
         * @param {String} [val] A string of text to set as the content of each matched element.
         * @return {String|undefined}
         */
        text: function (selector, val) {
            var el, els, i, nodeType;
            // getter
            if (val === undefined) {
                // supports css selector/Node/NodeList
                el = Dom.get(selector);
                return Dom._getText(el);
            } else {
                els = Dom.query(selector);
                for (i = els.length - 1; i >= 0; i--) {
                    el = els[i];
                    nodeType = el.nodeType;
                    if (nodeType === NodeType.ELEMENT_NODE) {
                        Dom.cleanData(el.getElementsByTagName('*'));
                        if ('textContent' in el) {
                            el.textContent = val;
                        } else {
                            el.innerText = val;
                        }
                    } else if (nodeType === NodeType.TEXT_NODE || nodeType === NodeType.CDATA_SECTION_NODE) {
                        el.nodeValue = val;
                    }
                }
            }
            return undefined;
        },

        _getText: function (el) {
            return el.textContent;
        }
    });

/*
 NOTES:
 yiminghe@gmail.com: 2013-03-19
 - boolean property 和 attribute ie 和其他浏览器不一致，统一为类似 ie8：
 - attr('checked',string) == .checked=true setAttribute('checked','checked') // ie8 相同 setAttribute()
 - attr('checked',false) == removeAttr('check') // ie8 不同, setAttribute ie8 相当于 .checked=true setAttribute('checked','checked')
 - removeAttr('checked') == .checked=false removeAttribute('checked') // ie8 removeAttribute 相同

 yiminghe@gmail.com: 2012-11-27
 - 拆分 ie attr，条件加载

 yiminghe@gmail.com：2011-06-03
 - 借鉴 jquery 1.6,理清 attribute 与 property

 yiminghe@gmail.com：2011-01-28
 - 处理 tabindex，顺便重构

 2010.03
 - 在 jquery/support.js 中，special attrs 里还有 maxlength, cellspacing,
 rowspan, colspan, usemap, frameborder, 但测试发现，在 Grade-A 级浏览器中
 并无兼容性问题。
 - 当 colspan/rowspan 属性值设置有误时，ie7- 会自动纠正，和 href 一样，需要传递
 第 2 个参数来解决。jQuery 未考虑，存在兼容性 bug.
 - jQuery 考虑了未显式设定 tabindex 时引发的兼容问题，kissy 里忽略（太不常用了）
 - jquery/attributes.js: Safari mis-reports the default selected
 property of an option 在 Safari 4 中已修复。

 */